总结开发习惯:关于编码规范、组件拆分思路、事件传递
完成 Menu、Layout、Header 三大核心组件后,回顾开发过程中反复出现的模式和踩过的坑,总结出一套可复用的开发习惯。这些习惯不是教条,而是在实战中反复验证过的最佳实践。
组件拆分思路
拆分时机
组件拆分不应该提前——先在一个文件中写完功能,当模板超过 100 行或 v-if/v-else 分支超过 3 个时,考虑拆分。
典型的拆分信号:
| 信号 | 示例 | 处理方式 |
|---|---|---|
| 模板条件分支多 | SubMenu 中的 item/subMenu 判断 | 拆出 MenuItem 组件 |
| 重复的 DOM 结构 | Header 中的多个工具组件 | 抽取公共样式组件 |
| Props 超过 8 个 | AvatarMenu 的配置项过多 | 使用配置对象合并 |
| 文件超过 200 行 | Layout 组件包含过多逻辑 | 拆出 composable |
拆分原则
基础组件(不依赖业务逻辑)
└── Menu、MenuItem、SubMenu、AvatarMenu
└── 独立可复用,不引入 Pinia、Router 等外部依赖
业务组件(组合基础组件)
└── Header、ThemeSettings
└── 可引入其他基础组件,但仍然保持独立性
布局组件(组合业务组件)
└── DefaultLayout
└── 编排所有组件,管理状态和事件
text
事件传递的三种模式
模式一:逐层 emit(适用于层级 <= 3)
AvatarMenu → emit('command') → Header → emit('command') → DefaultLayout
text
每个中间层都需要 defineEmits 和事件转发。虽然繁琐,但保持了组件的独立性和类型安全。
模式二:defineModel(适用于父子双向绑定)
// Header
const collapseModel = defineModel<boolean>('collapse')
typescript
适用于"父组件传入值 + 子组件修改值"的场景,如折叠状态、开关等。
模式三:Provide/Inject(适用于层级 >= 4 或跨多级传递)
// Layout
provide('themeSettings', readonly(localSettings))
// 任意子组件
const settings = inject('themeSettings')
typescript
适用于"一处提供、多处消费"的场景,如主题配置、国际化设置等。
先写后改的开发节奏
在实际开发中,很少能一次写出完美的代码。常见的开发节奏是:
- 先跑通:用最简单的方式实现功能,不考虑抽象和复用。
- 发现问题:模板膨胀、Props 过多、逻辑重复。
- 重构优化:拆分组件、提取 composable、抽象类型。
- 验证回归:确保重构后功能不受影响。
Menu 组件的开发过程就是这种节奏的典型体现——最初所有逻辑都在一个文件中,随着功能增加逐步拆出 SubMenu、MenuItem、useMenu。
TypeScript 类型设计习惯
善用类型别名
// 不要到处写完整类型
const columns: TableColumnCtx<any>[] = [...]
// 定义别名
export type TableColumnType = TableColumnCtx<any>
const columns: TableColumnType[] = [...]
typescript
Partial 和 Omit 灵活运用
// 大部分场景用 Partial
interface HeaderProps {
collapse?: boolean
username?: string
}
// 属性冲突时用 Omit
interface AvatarMenuProps
extends DropMenuProps,
Omit<AvatarProps, 'size'> {}
typescript
索引签名兜底
interface RootMeta {
title?: string
icon?: string
// 允许用户自定义属性
[key: string]: unknown
}
typescript
CSS 样式组织
深度选择器的使用原则
只在以下场景使用 :deep():
- 覆盖第三方组件(Element Plus)的内部样式
- 需要穿透
scoped样式边界 - 没有其他方式可以设置样式
// 合理使用
:deep(.el-menu-item__content) {
justify-content: flex-end;
}
// 不推荐:过度覆盖
:deep(.el-button) {
font-size: 14px; // 应该通过 props 或 class 设置
}
scss
CSS 变量优先
优先使用 Element Plus 提供的 CSS 变量,而非硬编码颜色值:
// 正确
background: var(--el-color-primary-light-9);
// 错误
background: #ecf5ff;
scss
本节小结
- 拆分时机:先在一个文件中实现功能,满足拆分信号时再拆分。
- 事件模式:逐层 emit(简单)、defineModel(双向)、provide/inject(跨级)。
- 开发节奏:先跑通、再发现问题、然后重构、最后验证。
- 类型设计:善用别名、Partial、Omit 和索引签名。
- 样式组织:优先使用 CSS 变量,减少
:deep()的使用。
↑